Hệ thống xếp lịch học tín chỉ cho sinh viên CNTT trên PHP & MySQL
112.134 lượt xem;
1 <?php
2 $admin_dir = dirname(__FILE__);
3 require("{$admin_dir}/incCommon.php");
4
5 $GLOBALS['DEBUG_MODE'] = false;
6 $csv = new Backup($_REQUEST);
7
8 class Backup{
9 private $curr_dir,
10 $curr_page,
11 $lang, /* translation text */
12 $request, /* assoc array that stores $_REQUEST */
13 $error_back_link,
14 $backup_log,
15 $initial_ts; /* initial timestamp */
16
17 public function __construct($request = array()){
18 global $Translation;
19
20 $this->curr_dir = dirname(__FILE__);
21 $this->curr_page = basename(__FILE__);
22 $this->initial_ts = microtime(true);
23 $this->lang = $Translation;
24
25 /* back link to use in errors */
26 $this->error_back_link = '' .
27 '<div class="text-center vspacer-lg"><a href="' . $this->curr_page . '" class="btn btn-danger btn-lg">' .
28 '<i class="glyphicon glyphicon-chevron-left"></i> ' .
29 $this->lang['back and retry'] .
30 '</a></div>';
31
32 /* create backup folder if needed */
33 if(!$this->create_backup_folder() && is_xhr()){
34 @header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error");
35 return;
36 }
37
38 /* process request to retrieve $this->request, and then execute the requested action */
39 $this->process_request($request);
40 $out = call_user_func_array(array($this, $this->request['action']), array());
41 if($out === true || $out === false){
42 echo $this->backup_log;
43 if(!$out) @header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error");
44 return;
45 }
46 echo $out;
47 }
48
49 protected function debug($msg, $html = true){
50 if($GLOBALS['DEBUG_MODE'] && $html) return "<pre>DEBUG: {$msg}</pre>";
51 if($GLOBALS['DEBUG_MODE']) return " [DEBUG: {$msg}] ";
52 return '';
53 }
54
55 protected function elapsed(){
56 return number_format(microtime(true) - $this->initial_ts, 3);
57 }
58
59 protected function process_request($request){
60 /* action must be a valid controller, else set to default (main) */
61 $controller = isset($request['action']) ? $request['action'] : false;
62 if(!in_array($controller, $this->controllers())) $request['action'] = 'main';
63
64 $this->request = $request;
65 }
66
67 /**
68 * discover the public functions in this class that can act as controllers
69 *
70 * @return array of public function names
71 */
72 protected function controllers(){
73 $csv = new ReflectionClass($this);
74 $methods = $csv->getMethods(ReflectionMethod::IS_PUBLIC);
75
76 $controllers = array();
77 foreach($methods as $mthd){
78 $controllers[] = $mthd->name;
79 }
80
81 return $controllers;
82 }
83
84 protected function request_or($var, $default){
85 return (isset($this->request[$var]) ? $this->request[$var] : $default);
86 }
87
88 protected function header(){
89 $Translation = $this->lang;
90 ob_start();
91 $GLOBALS['page_title'] = $Translation['database backups'];
92 include("{$this->curr_dir}/incHeader.php");
93 $out = ob_get_contents();
94 ob_end_clean();
95
96 return $out;
97 }
98
99 protected function footer(){
100 $Translation = $this->lang;
101 ob_start();
102 include("{$this->curr_dir}/incFooter.php");
103 $out = ob_get_contents();
104 ob_end_clean();
105
106 return $out;
107 }
108
109 /**
110 * @brief UTF8-encodes a string/array
111 * @see https://stackoverflow.com/a/26760943/1945185
112 *
113 * @param [in] $mixed string or array of strings to be UTF8-encoded
114 * @return UTF8-encoded array/string
115 */
116 protected function utf8ize($mixed) {
117 if(!is_array($mixed)) return to_utf8($mixed);
118
119 foreach($mixed as $key => $value){
120 $mixed[$key] = $this->utf8ize($value);
121 }
122 return $mixed;
123 }
124
125 /**
126 * @brief Retrieves and validates user-specified md5_hash and checks if it matches a backup file
127 *
128 * @return False on error, backup file full path on success.
129 */
130 protected function get_specified_backup_file(){
131 $md5_hash = $this->request['md5_hash'];
132 if(!preg_match('/^[a-f0-9]{32}$/i', $md5_hash)) return false;
133
134 $bfile = "{$this->curr_dir}/backups/{$md5_hash}.sql";
135 if(!is_file($bfile)) return false;
136
137 return $bfile;
138 }
139
140 /**
141 * function to show main page
142 */
143 public function main(){
144 ob_start();
145
146 echo $this->header();
147
148 $can_backup = $this->create_backup_folder();
149
150 ?>
151 <div class="page-header"><h1><?php echo $this->lang['database backups']; ?></h1></div>
152
153 <div id="in-page-notifications"></div>
154 <script>
155 /* move notifications below page title */
156 $j(function(){
157 $j('.notifcation-placeholder').appendTo('#in-page-notifications');
158 })
159 </script>
160
161 <?php
162 echo Notification::show(array(
163 'message' => '<i class="glyphicon glyphicon-info-sign"></i> ' . $this->lang['about backups'],
164 'class' => 'info',
165 'dismiss_days' => 30,
166 'id' => 'info-about-backups'
167 ));
168
169 if(!$can_backup){
170 echo Notification::show(array(
171 'message' => $this->lang['cant create backup folder'],
172 'class' => 'danger',
173 'dismiss_seconds' => 900
174 ));
175 }
176 ?>
177
178 <?php if($can_backup){ ?>
179 <button type="button" class="vspacer-lg btn btn-primary btn-lg" id="create-backup"><i class="glyphicon glyphicon-plus"></i> <?php echo $this->lang['create backup file']; ?></button>
180 <pre id="backup-log" class="hidden"></pre>
181
182 <h2><?php echo $this->lang['available backups']; ?></h2>
183 <div id="backup-files-list"></div>
184
185 <style>
186 .backup-file{
187 border-bottom: solid 1px #aaa;
188 padding: .75em;
189 margin: 0;
190 }
191 </style>
192 <script>
193 $j(function(){
194 /* language strings */
195 var create_backup = '<?php echo html_attr($this->lang['create backup file']); ?>';
196 var please_wait = '<?php echo html_attr($this->lang['please wait']); ?>';
197 var finished = '<?php echo html_attr($this->lang['done!']); ?>';
198 var error = '<?php echo html_attr($this->lang['error']); ?>';
199 var no_matches = '<?php echo html_attr($this->lang['no backups found']); ?>';
200 var restore_backup = '<?php echo html_attr($this->lang['restore backup']); ?>';
201 var delete_backup = '<?php echo html_attr($this->lang['delete backup']); ?>';
202 var confirm_backup = '<?php echo html_attr($this->lang['confirm backup']); ?>';
203 var confirm_restore = '<?php echo html_attr($this->lang['confirm restore']); ?>';
204 var confirm_delete = '<?php echo html_attr($this->lang['confirm delete backup']); ?>';
205 var backup_restored = '<?php echo html_attr($this->lang['backup restored']); ?>';
206 var backup_deleted = '<?php echo html_attr($this->lang['backup deleted']); ?>';
207 var delete_error = '<?php echo html_attr($this->lang['backup delete error']); ?>';
208 var restore_error = '<?php echo html_attr($this->lang['restore error']); ?>';
209
210 var page = '<?php echo $this->curr_page; ?>';
211 var backup_files_list = $j('#backup-files-list');
212
213 var clear_list = function(){
214 backup_files_list.html('<div class="alert alert-warning">' + no_matches + '</div>');
215 }
216
217 var display_backups = function(){
218 $j.ajax({
219 url: page,
220 data: { action: 'get_backup_files' },
221 success: function(resp){
222 try{
223 var list = JSON.parse(resp);
224 if(list.constructor !== Array) throw 'not a list of files';
225
226 backup_files_list.html('');
227 for(var i = 0; i < list.length; i++){
228 backup_files_list.append(
229 '<h4 class="hspacer-lg backup-file">' +
230 '<div class="btn-group hspacer-lg">' +
231 '<button type="button" class="btn btn-default restore" data-md5_hash="' + list[i].md5_hash + '"><i class="glyphicon glyphicon-download-alt"></i> ' + restore_backup + '</button>' +
232 '<button type="button" class="btn btn-default delete" data-md5_hash="' + list[i].md5_hash + '"><i class="glyphicon glyphicon-trash"></i> ' + delete_backup + '</button>' +
233 '</div>' +
234 list[i].datetime +
235 ' (' + list[i].size + ' KB) ' +
236 '</h4>'
237 );
238 }
239 }catch(e){
240 clear_list();
241 }
242 },
243 error: clear_list
244 });
245 };
246
247 backup_files_list
248 .on('click', '.restore', function(){
249 /* confirm restore */
250 if(!confirm(confirm_restore)) return;
251
252 $j.ajax({
253 url: page,
254 data: { action: 'restore', md5_hash: $j(this).data('md5_hash') },
255 success: function(){
256 show_notification({
257 message: backup_restored,
258 class: 'success',
259 dismiss_seconds: 30
260 });
261 },
262 error: function(){
263 show_notification({
264 message: restore_error,
265 class: 'danger',
266 dismiss_seconds: 30
267 });
268 },
269 complete: display_backups
270 });
271 })
272 .on('click', '.delete', function(){
273 /* confirm delete backup */
274 if(!confirm(confirm_delete)) return;
275
276 $j.ajax({
277 url: page,
278 data: { action: 'delete', md5_hash: $j(this).data('md5_hash') },
279 success: function(){
280 show_notification({
281 message: backup_deleted,
282 class: 'success',
283 dismiss_seconds: 30
284 });
285 },
286 error: function(){
287 show_notification({
288 message: delete_error,
289 class: 'danger',
290 dismiss_seconds: 30
291 });
292 },
293 complete: display_backups
294 });
295 })
296 .on('mouseover', 'h4', function(){
297 $j(this).addClass('bg-warning');
298 })
299 .on('mouseout', 'h4', function(){
300 $j(this).removeClass('bg-warning');
301 });
302
303 $j('#create-backup').click(function(){
304 if(!confirm(confirm_backup)) return;
305
306 $j('#backup-log').html('').addClass('hidden');
307
308 var btn = $j(this);
309 btn.addClass('btn-warning').prop('disabled', true).html('<i class="glyphicon glyphicon-hourglass"></i> ' + please_wait);
310 $j.ajax({
311 url: page,
312 data: { action: 'create_backup' },
313 success: function(){
314 btn.removeClass('btn-warning btn-primary').addClass('btn-success').html('<i class="glyphicon glyphicon-ok"></i> ' + finished);
315 },
316 error: function(){
317 btn.removeClass('btn-warning btn-primary').addClass('btn-danger').html('<i class="glyphicon glyphicon-remove"></i> ' + error);
318 },
319 complete: function(jx){
320 if(jx.responseText.length > 0) $j('#backup-log').html(jx.responseText).removeClass('hidden');
321 display_backups();
322 setTimeout(function(){
323 btn.removeClass('btn-danger btn-warning').addClass('btn-primary').prop('disabled', false).html('<i class="glyphicon glyphicon-plus"></i> ' + create_backup);
324 }, 10000);
325 }
326 });
327 });
328
329 /* keep removing dismissed notifications from DOM */
330 setInterval(function(){
331 $j('.notifcation-placeholder .invisible').remove();
332 }, 1000);
333
334 display_backups();
335 })
336 </script>
337 <?php } ?>
338
339 <?php
340 echo $this->footer();
341
342 $html = ob_get_clean();
343
344 return $html;
345 }
346
347 /**
348 * @brief Retrieve a list of available backup files, with dates and times
349 *
350 * @return Array of backup files [[md5_hash => '', datetime => 'y-m-d H:i:s', size => '659888'], ..]
351 *
352 * @details Backup files are those found in the folder 'backups' named as an md5 hash with .sql extension.
353 */
354 public function get_backup_files(){
355 $bdir = $this->curr_dir . '/backups';
356 $d = dir($bdir);
357 if(!$d) return false;
358 $admin_cfg = config('adminConfig');
359 $dtf = $admin_cfg['PHPDateTimeFormat'];
360
361 $list = array();
362
363 while(false !== ($entry = $d->read())){
364 if(!preg_match('/^[a-f0-9]{32}\.sql$/i', $entry)) continue;
365 $fts = @filemtime("{$bdir}/{$entry}");
366 $list[$fts] = array(
367 'md5_hash' => substr($entry, 0, 32),
368 'datetime' => date($dtf, $fts),
369 'size' => number_format(@filesize("{$bdir}/{$entry}") / 1024)
370 );
371 }
372
373 $d->close();
374 if(!count($list)) return false;
375
376 krsort($list);
377 return json_encode(array_values($list));
378 }
379
380 /**
381 * @brief create a new backup file
382 *
383 * @return Boolean indicating success or failure
384 *
385 * @details Uses mysqldump (if available) to create a new backup file
386 */
387 public function create_backup(){
388 $config = array('dbServer' => '', 'dbUsername' => '', 'dbPassword' => '', 'dbDatabase' => '');
389 foreach($config as $k => $v) $config[$k] = escapeshellarg(config($k));
390
391 $dump_file = $this->curr_dir . '/backups/' . md5(microtime()) . '.sql';
392 $out = array(); $ret = 0;
393 maintenance_mode(true);
394 $pass_param = ($config['dbPassword'] ? "-p{$config['dbPassword']}" : '');
395 @exec("(mysqldump -u{$config['dbUsername']} {$pass_param} -h{$config['dbServer']} {$config['dbDatabase']} > {$dump_file}) 2>&1", $out, $ret);
396 $this->backup_log = implode("\n", $out);
397 maintenance_mode(false);
398
399 if($ret) return false;
400
401 return true;
402 }
403
404 /**
405 * @brief Restores a given backup file
406 *
407 * @return Boolean indicating success or failure
408 *
409 * @details Overwrites existing data in the database, including users and groups.
410 */
411 public function restore(){
412 $bfile = $this->get_specified_backup_file();
413 if(!$bfile) return false;
414
415 $config = array('dbServer' => '', 'dbUsername' => '', 'dbPassword' => '', 'dbDatabase' => '');
416 foreach($config as $k => $v) $config[$k] = config($k);
417
418 $out = $ret = null;
419 maintenance_mode(true);
420 $cmd = "mysql -u{$config['dbUsername']} -p{$config['dbPassword']} -h{$config['dbServer']} {$config['dbDatabase']} < {$bfile}";
421 @exec($cmd, $out, $ret);
422 maintenance_mode(false);
423
424 if($ret){ echo $cmd; return false; }
425
426 return true;
427 }
428
429 /**
430 * @brief Deletes a given backup file
431 *
432 * @return Boolean indicating success or failure
433 */
434 public function delete(){
435 $bfile = $this->get_specified_backup_file();
436 if(!$bfile) return false;
437
438 return @unlink($bfile);
439 }
440
441 protected function create_backup_folder(){
442 $bdir = $this->curr_dir . '/backups';
443 if(!is_dir($bdir))
444 if(!@mkdir($bdir)) return false;
445
446 /* create .htaccess file preventing direct download of backup files */
447 if(!is_file("{$bdir}/.htaccess"))
448 @file_put_contents("{$bdir}/.htaccess",
449 "<FilesMatch \"\\.(sql)\$\">\n" .
450 " Order allow,deny\n" .
451 "</FilesMatch>"
452 );
453
454 /* create index.html empty file to prevent directory browsing */
455 if(!is_file("{$bdir}/index.html")) @touch("{$bdir}/index.html");
456
457 return true;
458 }
459 }
2 $admin_dir = dirname(__FILE__);
3 require("{$admin_dir}/incCommon.php");
4
5 $GLOBALS['DEBUG_MODE'] = false;
6 $csv = new Backup($_REQUEST);
7
8 class Backup{
9 private $curr_dir,
10 $curr_page,
11 $lang, /* translation text */
12 $request, /* assoc array that stores $_REQUEST */
13 $error_back_link,
14 $backup_log,
15 $initial_ts; /* initial timestamp */
16
17 public function __construct($request = array()){
18 global $Translation;
19
20 $this->curr_dir = dirname(__FILE__);
21 $this->curr_page = basename(__FILE__);
22 $this->initial_ts = microtime(true);
23 $this->lang = $Translation;
24
25 /* back link to use in errors */
26 $this->error_back_link = '' .
27 '<div class="text-center vspacer-lg"><a href="' . $this->curr_page . '" class="btn btn-danger btn-lg">' .
28 '<i class="glyphicon glyphicon-chevron-left"></i> ' .
29 $this->lang['back and retry'] .
30 '</a></div>';
31
32 /* create backup folder if needed */
33 if(!$this->create_backup_folder() && is_xhr()){
34 @header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error");
35 return;
36 }
37
38 /* process request to retrieve $this->request, and then execute the requested action */
39 $this->process_request($request);
40 $out = call_user_func_array(array($this, $this->request['action']), array());
41 if($out === true || $out === false){
42 echo $this->backup_log;
43 if(!$out) @header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error");
44 return;
45 }
46 echo $out;
47 }
48
49 protected function debug($msg, $html = true){
50 if($GLOBALS['DEBUG_MODE'] && $html) return "<pre>DEBUG: {$msg}</pre>";
51 if($GLOBALS['DEBUG_MODE']) return " [DEBUG: {$msg}] ";
52 return '';
53 }
54
55 protected function elapsed(){
56 return number_format(microtime(true) - $this->initial_ts, 3);
57 }
58
59 protected function process_request($request){
60 /* action must be a valid controller, else set to default (main) */
61 $controller = isset($request['action']) ? $request['action'] : false;
62 if(!in_array($controller, $this->controllers())) $request['action'] = 'main';
63
64 $this->request = $request;
65 }
66
67 /**
68 * discover the public functions in this class that can act as controllers
69 *
70 * @return array of public function names
71 */
72 protected function controllers(){
73 $csv = new ReflectionClass($this);
74 $methods = $csv->getMethods(ReflectionMethod::IS_PUBLIC);
75
76 $controllers = array();
77 foreach($methods as $mthd){
78 $controllers[] = $mthd->name;
79 }
80
81 return $controllers;
82 }
83
84 protected function request_or($var, $default){
85 return (isset($this->request[$var]) ? $this->request[$var] : $default);
86 }
87
88 protected function header(){
89 $Translation = $this->lang;
90 ob_start();
91 $GLOBALS['page_title'] = $Translation['database backups'];
92 include("{$this->curr_dir}/incHeader.php");
93 $out = ob_get_contents();
94 ob_end_clean();
95
96 return $out;
97 }
98
99 protected function footer(){
100 $Translation = $this->lang;
101 ob_start();
102 include("{$this->curr_dir}/incFooter.php");
103 $out = ob_get_contents();
104 ob_end_clean();
105
106 return $out;
107 }
108
109 /**
110 * @brief UTF8-encodes a string/array
111 * @see https://stackoverflow.com/a/26760943/1945185
112 *
113 * @param [in] $mixed string or array of strings to be UTF8-encoded
114 * @return UTF8-encoded array/string
115 */
116 protected function utf8ize($mixed) {
117 if(!is_array($mixed)) return to_utf8($mixed);
118
119 foreach($mixed as $key => $value){
120 $mixed[$key] = $this->utf8ize($value);
121 }
122 return $mixed;
123 }
124
125 /**
126 * @brief Retrieves and validates user-specified md5_hash and checks if it matches a backup file
127 *
128 * @return False on error, backup file full path on success.
129 */
130 protected function get_specified_backup_file(){
131 $md5_hash = $this->request['md5_hash'];
132 if(!preg_match('/^[a-f0-9]{32}$/i', $md5_hash)) return false;
133
134 $bfile = "{$this->curr_dir}/backups/{$md5_hash}.sql";
135 if(!is_file($bfile)) return false;
136
137 return $bfile;
138 }
139
140 /**
141 * function to show main page
142 */
143 public function main(){
144 ob_start();
145
146 echo $this->header();
147
148 $can_backup = $this->create_backup_folder();
149
150 ?>
151 <div class="page-header"><h1><?php echo $this->lang['database backups']; ?></h1></div>
152
153 <div id="in-page-notifications"></div>
154 <script>
155 /* move notifications below page title */
156 $j(function(){
157 $j('.notifcation-placeholder').appendTo('#in-page-notifications');
158 })
159 </script>
160
161 <?php
162 echo Notification::show(array(
163 'message' => '<i class="glyphicon glyphicon-info-sign"></i> ' . $this->lang['about backups'],
164 'class' => 'info',
165 'dismiss_days' => 30,
166 'id' => 'info-about-backups'
167 ));
168
169 if(!$can_backup){
170 echo Notification::show(array(
171 'message' => $this->lang['cant create backup folder'],
172 'class' => 'danger',
173 'dismiss_seconds' => 900
174 ));
175 }
176 ?>
177
178 <?php if($can_backup){ ?>
179 <button type="button" class="vspacer-lg btn btn-primary btn-lg" id="create-backup"><i class="glyphicon glyphicon-plus"></i> <?php echo $this->lang['create backup file']; ?></button>
180 <pre id="backup-log" class="hidden"></pre>
181
182 <h2><?php echo $this->lang['available backups']; ?></h2>
183 <div id="backup-files-list"></div>
184
185 <style>
186 .backup-file{
187 border-bottom: solid 1px #aaa;
188 padding: .75em;
189 margin: 0;
190 }
191 </style>
192 <script>
193 $j(function(){
194 /* language strings */
195 var create_backup = '<?php echo html_attr($this->lang['create backup file']); ?>';
196 var please_wait = '<?php echo html_attr($this->lang['please wait']); ?>';
197 var finished = '<?php echo html_attr($this->lang['done!']); ?>';
198 var error = '<?php echo html_attr($this->lang['error']); ?>';
199 var no_matches = '<?php echo html_attr($this->lang['no backups found']); ?>';
200 var restore_backup = '<?php echo html_attr($this->lang['restore backup']); ?>';
201 var delete_backup = '<?php echo html_attr($this->lang['delete backup']); ?>';
202 var confirm_backup = '<?php echo html_attr($this->lang['confirm backup']); ?>';
203 var confirm_restore = '<?php echo html_attr($this->lang['confirm restore']); ?>';
204 var confirm_delete = '<?php echo html_attr($this->lang['confirm delete backup']); ?>';
205 var backup_restored = '<?php echo html_attr($this->lang['backup restored']); ?>';
206 var backup_deleted = '<?php echo html_attr($this->lang['backup deleted']); ?>';
207 var delete_error = '<?php echo html_attr($this->lang['backup delete error']); ?>';
208 var restore_error = '<?php echo html_attr($this->lang['restore error']); ?>';
209
210 var page = '<?php echo $this->curr_page; ?>';
211 var backup_files_list = $j('#backup-files-list');
212
213 var clear_list = function(){
214 backup_files_list.html('<div class="alert alert-warning">' + no_matches + '</div>');
215 }
216
217 var display_backups = function(){
218 $j.ajax({
219 url: page,
220 data: { action: 'get_backup_files' },
221 success: function(resp){
222 try{
223 var list = JSON.parse(resp);
224 if(list.constructor !== Array) throw 'not a list of files';
225
226 backup_files_list.html('');
227 for(var i = 0; i < list.length; i++){
228 backup_files_list.append(
229 '<h4 class="hspacer-lg backup-file">' +
230 '<div class="btn-group hspacer-lg">' +
231 '<button type="button" class="btn btn-default restore" data-md5_hash="' + list[i].md5_hash + '"><i class="glyphicon glyphicon-download-alt"></i> ' + restore_backup + '</button>' +
232 '<button type="button" class="btn btn-default delete" data-md5_hash="' + list[i].md5_hash + '"><i class="glyphicon glyphicon-trash"></i> ' + delete_backup + '</button>' +
233 '</div>' +
234 list[i].datetime +
235 ' (' + list[i].size + ' KB) ' +
236 '</h4>'
237 );
238 }
239 }catch(e){
240 clear_list();
241 }
242 },
243 error: clear_list
244 });
245 };
246
247 backup_files_list
248 .on('click', '.restore', function(){
249 /* confirm restore */
250 if(!confirm(confirm_restore)) return;
251
252 $j.ajax({
253 url: page,
254 data: { action: 'restore', md5_hash: $j(this).data('md5_hash') },
255 success: function(){
256 show_notification({
257 message: backup_restored,
258 class: 'success',
259 dismiss_seconds: 30
260 });
261 },
262 error: function(){
263 show_notification({
264 message: restore_error,
265 class: 'danger',
266 dismiss_seconds: 30
267 });
268 },
269 complete: display_backups
270 });
271 })
272 .on('click', '.delete', function(){
273 /* confirm delete backup */
274 if(!confirm(confirm_delete)) return;
275
276 $j.ajax({
277 url: page,
278 data: { action: 'delete', md5_hash: $j(this).data('md5_hash') },
279 success: function(){
280 show_notification({
281 message: backup_deleted,
282 class: 'success',
283 dismiss_seconds: 30
284 });
285 },
286 error: function(){
287 show_notification({
288 message: delete_error,
289 class: 'danger',
290 dismiss_seconds: 30
291 });
292 },
293 complete: display_backups
294 });
295 })
296 .on('mouseover', 'h4', function(){
297 $j(this).addClass('bg-warning');
298 })
299 .on('mouseout', 'h4', function(){
300 $j(this).removeClass('bg-warning');
301 });
302
303 $j('#create-backup').click(function(){
304 if(!confirm(confirm_backup)) return;
305
306 $j('#backup-log').html('').addClass('hidden');
307
308 var btn = $j(this);
309 btn.addClass('btn-warning').prop('disabled', true).html('<i class="glyphicon glyphicon-hourglass"></i> ' + please_wait);
310 $j.ajax({
311 url: page,
312 data: { action: 'create_backup' },
313 success: function(){
314 btn.removeClass('btn-warning btn-primary').addClass('btn-success').html('<i class="glyphicon glyphicon-ok"></i> ' + finished);
315 },
316 error: function(){
317 btn.removeClass('btn-warning btn-primary').addClass('btn-danger').html('<i class="glyphicon glyphicon-remove"></i> ' + error);
318 },
319 complete: function(jx){
320 if(jx.responseText.length > 0) $j('#backup-log').html(jx.responseText).removeClass('hidden');
321 display_backups();
322 setTimeout(function(){
323 btn.removeClass('btn-danger btn-warning').addClass('btn-primary').prop('disabled', false).html('<i class="glyphicon glyphicon-plus"></i> ' + create_backup);
324 }, 10000);
325 }
326 });
327 });
328
329 /* keep removing dismissed notifications from DOM */
330 setInterval(function(){
331 $j('.notifcation-placeholder .invisible').remove();
332 }, 1000);
333
334 display_backups();
335 })
336 </script>
337 <?php } ?>
338
339 <?php
340 echo $this->footer();
341
342 $html = ob_get_clean();
343
344 return $html;
345 }
346
347 /**
348 * @brief Retrieve a list of available backup files, with dates and times
349 *
350 * @return Array of backup files [[md5_hash => '', datetime => 'y-m-d H:i:s', size => '659888'], ..]
351 *
352 * @details Backup files are those found in the folder 'backups' named as an md5 hash with .sql extension.
353 */
354 public function get_backup_files(){
355 $bdir = $this->curr_dir . '/backups';
356 $d = dir($bdir);
357 if(!$d) return false;
358 $admin_cfg = config('adminConfig');
359 $dtf = $admin_cfg['PHPDateTimeFormat'];
360
361 $list = array();
362
363 while(false !== ($entry = $d->read())){
364 if(!preg_match('/^[a-f0-9]{32}\.sql$/i', $entry)) continue;
365 $fts = @filemtime("{$bdir}/{$entry}");
366 $list[$fts] = array(
367 'md5_hash' => substr($entry, 0, 32),
368 'datetime' => date($dtf, $fts),
369 'size' => number_format(@filesize("{$bdir}/{$entry}") / 1024)
370 );
371 }
372
373 $d->close();
374 if(!count($list)) return false;
375
376 krsort($list);
377 return json_encode(array_values($list));
378 }
379
380 /**
381 * @brief create a new backup file
382 *
383 * @return Boolean indicating success or failure
384 *
385 * @details Uses mysqldump (if available) to create a new backup file
386 */
387 public function create_backup(){
388 $config = array('dbServer' => '', 'dbUsername' => '', 'dbPassword' => '', 'dbDatabase' => '');
389 foreach($config as $k => $v) $config[$k] = escapeshellarg(config($k));
390
391 $dump_file = $this->curr_dir . '/backups/' . md5(microtime()) . '.sql';
392 $out = array(); $ret = 0;
393 maintenance_mode(true);
394 $pass_param = ($config['dbPassword'] ? "-p{$config['dbPassword']}" : '');
395 @exec("(mysqldump -u{$config['dbUsername']} {$pass_param} -h{$config['dbServer']} {$config['dbDatabase']} > {$dump_file}) 2>&1", $out, $ret);
396 $this->backup_log = implode("\n", $out);
397 maintenance_mode(false);
398
399 if($ret) return false;
400
401 return true;
402 }
403
404 /**
405 * @brief Restores a given backup file
406 *
407 * @return Boolean indicating success or failure
408 *
409 * @details Overwrites existing data in the database, including users and groups.
410 */
411 public function restore(){
412 $bfile = $this->get_specified_backup_file();
413 if(!$bfile) return false;
414
415 $config = array('dbServer' => '', 'dbUsername' => '', 'dbPassword' => '', 'dbDatabase' => '');
416 foreach($config as $k => $v) $config[$k] = config($k);
417
418 $out = $ret = null;
419 maintenance_mode(true);
420 $cmd = "mysql -u{$config['dbUsername']} -p{$config['dbPassword']} -h{$config['dbServer']} {$config['dbDatabase']} < {$bfile}";
421 @exec($cmd, $out, $ret);
422 maintenance_mode(false);
423
424 if($ret){ echo $cmd; return false; }
425
426 return true;
427 }
428
429 /**
430 * @brief Deletes a given backup file
431 *
432 * @return Boolean indicating success or failure
433 */
434 public function delete(){
435 $bfile = $this->get_specified_backup_file();
436 if(!$bfile) return false;
437
438 return @unlink($bfile);
439 }
440
441 protected function create_backup_folder(){
442 $bdir = $this->curr_dir . '/backups';
443 if(!is_dir($bdir))
444 if(!@mkdir($bdir)) return false;
445
446 /* create .htaccess file preventing direct download of backup files */
447 if(!is_file("{$bdir}/.htaccess"))
448 @file_put_contents("{$bdir}/.htaccess",
449 "<FilesMatch \"\\.(sql)\$\">\n" .
450 " Order allow,deny\n" .
451 "</FilesMatch>"
452 );
453
454 /* create index.html empty file to prevent directory browsing */
455 if(!is_file("{$bdir}/index.html")) @touch("{$bdir}/index.html");
456
457 return true;
458 }
459 }